﻿#region copyright
/*
* Copyright (c) 2009, Dion Kurczek
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the <organization> nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY DION KURCZEK ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL DION KURCZEK BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using SCG.General;

namespace SCG.Prism
{
    //PrismNetworkStream reads individual messages from a NetworkStream, and adopts the message
    //length specifier prefix when sending data and reading messages
    public class PrismNetworkStream
    {
        //Constructors
        //Assumes the socket is already connected and doesn't have an event handler attached
        public PrismNetworkStream(Socket socket)
        {
            _socket = socket;           

            //begin the writer thread
            Thread writer = new Thread(new ThreadStart(WriteThreadExecute));
            writer.IsBackground = true;
            writer.Start();
        }

        //this constructor allows the encoding to be something other than the default Unicode
        public PrismNetworkStream(Socket socket, Encoding encoding)
            : this(socket)
        {
            _encoding = encoding;
        }

        //Close the underlying stream, and socket
        public void Close()
        {
            //Set local flag so running thread can terminate during read operations
            _closed = true;

            //Close underlying socket
            _socket.Close();         
        }

        //Determine if the connection was closed explictly
        public bool WasClosed
        {
            get
            {
                return _closed;
            }
        }

        //Number of bytes read/written to stream
        public int BytesRead { get; set; }

        public int BytesWritten { get; set; }

        //PrismServer uses "Tokenized" input/output strings.  These strings contain a series of
        //pipe (|) delimited tokens.  Each token is composed of a length prefix, followed by the
        //pipe delimiter, follwed by the text of the token.  In this way, the pipe character itself
        //can also be safely used in messaging.
        public void WriteTokens(params object[] tokens)
        {
            Tokenizer tok = new Tokenizer();
            string s = tok.Tokenize(tokens);         
            lock (_outgoing)
            {
                _outgoing.Enqueue(s);
            }
        }

        //De-tokenize a string and return the main COMMAND string and a list of optional tokens
        public string ReadTokens(List<string> stringList)
        {
            string CommandString = "";
            stringList.Clear();
            string s = ReadString();

            if (s.Length > 0)
            {
                Tokenizer tok = new Tokenizer(s);
                CommandString = tok.TokenString;
                while (tok.HasTokens)
                    stringList.Add(tok.TokenString);
            }
            return CommandString;
        }

        //Private Members
        private const int BUFFER_SIZE = 1024;
        private Socket _socket;
        private int _desiredMessagelength = 0;
        private string _incomingMessage = "";
        private bool _closed = false;
        private Queue<string> _outgoing = new Queue<string>();
        private Encoding _encoding = Encoding.Unicode;
        private bool _readCompleted = false;
        private string _messageRead = "";

        //Read a string from the NetworkStream - this is called from a thread in PrismConnection component
        private string ReadString()
        {
            string ReturnString = "";
            int Pos;           

            do
            {
                //Do we have a message length already defined?
                if (_desiredMessagelength > 0)
                {
                    //Yes, does the string contain the full message?
                    if (_incomingMessage.Length >= _desiredMessagelength)
                    {
                        //Yes, pluck out the message
                        ReturnString = _incomingMessage.Substring(0, _desiredMessagelength);
                        _incomingMessage = _incomingMessage.Substring(_desiredMessagelength, _incomingMessage.Length - _desiredMessagelength);
                        _desiredMessagelength = 0;
                    }
                    else
                    {
                        //No, need to pull more data from socket and append to incoming string
                        _readCompleted = false;

                        SocketAsyncEventArgs sae = new SocketAsyncEventArgs();
                        CreateBuffer(sae);
                        sae.Completed += SocketASynchCompleted;
                        _socket.ReceiveAsync(sae);

                        while (!_readCompleted && !_closed)
                        {
                            Thread.Sleep(10);
                        }

                        sae.Completed -= SocketASynchCompleted;
                        sae.Dispose();
                        sae = null;

                        //throw exception when socket closed
                        if (_messageRead == "")
                            throw new InvalidOperationException("Socket Closed");

                        _incomingMessage += _messageRead;
                        _messageRead = "";                       
                    }
                }
                //No, is there a length specifier in the string?
                else
                {
                    Pos = _incomingMessage.IndexOf('|');
                    if (Pos > 0)
                    {
                        //We have a length specifier, parse it out and allow loop to continue
                        _desiredMessagelength = Int32.Parse(_incomingMessage.Substring(0, Pos));
                        _incomingMessage = _incomingMessage.Substring(Pos + 1, _incomingMessage.Length - Pos - 1);
                    }
                    else
                    {
                        //No length specifier - time to read some data from the socket
                        _readCompleted = false;

                        SocketAsyncEventArgs sae = new SocketAsyncEventArgs();
                        CreateBuffer(sae);
                        sae.Completed += SocketASynchCompleted;
                        _socket.ReceiveAsync(sae);

                        while (!_readCompleted && !_closed)
                        {
                            Thread.Sleep(10);
                        }

                        sae.Completed -= SocketASynchCompleted;
                        sae.Dispose();
                        sae = null;

                        _incomingMessage += _messageRead;
                        _messageRead = "";
                    }
                }
            } 
            while (ReturnString == "" && _socket.Connected && !_closed);
            return ReturnString;
        }

        //This thread keeps executing and writing information to the client
        private void WriteThreadExecute()
        {
            while (!_closed)
            {
                bool written = true;
                while (written)
                {
                    written = false;
                    string s = null;
                    lock (_outgoing)
                    {
                        if (_outgoing.Count > 0)
                            s = _outgoing.Dequeue();
                    }
                    if (s != null)
                    {
                        written = true;
                        WriteString(s);
                    }
                }
                Thread.Sleep(10);
            }
        }

        //Write a string to the NetworkStream
        private void WriteString(string s)
        {
            try
            {
                if (_socket.Connected)
                {
                    s = s.Length + "|" + s;
                    byte[] outputBytes = _encoding.GetBytes(s);
                    lock (this)
                    {
                        BytesWritten += outputBytes.Length;
                    }

                    List<ArraySegment<byte>> bl = new List<ArraySegment<byte>>();
                    bl.Add(new ArraySegment<byte>(outputBytes));

                    SocketAsyncEventArgs saeOut = new SocketAsyncEventArgs();
                    saeOut.Completed += saeOut_Completed;
                    saeOut.BufferList = bl;

                    _socket.SendAsync(saeOut);
                    return;
                }
            }
            catch
            {             
            }
        }

        //write operation completed
        private void saeOut_Completed(object sender, SocketAsyncEventArgs e)
        {
            e.Completed -= saeOut_Completed;
            e.Dispose();
        }

        //event handler for socket receiving data
        private void SocketASynchCompleted(object sender, SocketAsyncEventArgs e)
        {
            if (e.BytesTransferred == 0)
            {
                _messageRead = "";
                _readCompleted = true;
                return;
            }

            try
            {
                _messageRead = _encoding.GetString(e.BufferList[0].Array, e.Offset, e.BytesTransferred);
                lock (this)
                {
                    BytesRead += e.BytesTransferred;
                }
                _readCompleted = true;
            }
            catch
            {
            }
        }

        //Create buffer for SocketASynchEventArgs
        private void CreateBuffer(SocketAsyncEventArgs sae)
        {          
            byte[] buffer = new byte[BUFFER_SIZE];
            List<ArraySegment<byte>> bl = new List<ArraySegment<byte>>();  
            bl.Add(new ArraySegment<byte>(buffer));      
            sae.BufferList = bl;
        }
    }
}
